﻿<!-- 大文件分片上传 后台koa -->
<template>
    <div class="upload-container">
        <div class="upload-area" @click="triggerFileInput">
            <input ref="fileInput" type="file" @change="handleFileSelect" style="display: none" />
            <div class="upload-placeholder">
                <span>点击选择文件或拖拽文件到此区域</span>
            </div>
        </div>

        <div v-if="selectedFile" class="file-info">
            <h3>文件信息</h3>
            <p>文件名: {{ selectedFile.name }}</p>
            <p>文件大小: {{ formatFileSize(selectedFile.size) }}</p>
            <p>分片数量: {{ totalChunks }}</p>
        </div>

        <div class="upload-controls">
            <button @click="startUpload" :disabled="!selectedFile || isUploading" class="upload-btn">
                {{ isUploading ? '上传中...' : '开始上传' }}
            </button>
            <button @click="pauseUpload" :disabled="!isUploading" class="pause-btn">暂停上传</button>
        </div>

        <div v-if="isUploading" class="progress-section">
            <div class="progress-bar">
                <div class="progress-fill" :style="{ width: uploadProgress + '%' }"></div>
            </div>
            <span class="progress-text">{{ uploadProgress.toFixed(2) }}%</span>
        </div>

        <div v-if="uploadResult" class="result-section">
            <div :class="['result-message', uploadResult.success ? 'success' : 'error']">
                {{ uploadResult.message }}
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, computed, useTemplateRef } from 'vue';
import { calculateFileMD5 } from '@/utils/sprkmd5';
import api from '@/apis/upload';

// 响应式数据
const fileInput = useTemplateRef('fileInput');
const selectedFile = ref<File | any>();
const isUploading = ref(false);
const uploadProgress = ref(0);
const uploadResult = ref<any>(null);
const isPaused = ref(false);
const uploadChunks = ref([]); //已完成的chunks

// 常量配置
const CHUNK_SIZE = 1 * 1024 * 1024; // 1MB每片
const CONCURRENCY_LIMIT = 3; // 并发限制

// 计算属性
const totalChunks = computed(() => (selectedFile.value ? Math.ceil(selectedFile.value.size / CHUNK_SIZE) : 0));

// 文件选择处理
const handleFileSelect = (event: any) => {
    const file = event.target.files[0];
    if (!file) return;

    selectedFile.value = file;
    uploadProgress.value = 0;
    uploadResult.value = null;
    isPaused.value = false;

    // console.log(`选中文件: ${file.name}, 大小: ${file.size} bytes`);
};

// 触发文件选择
const triggerFileInput = () => {
    fileInput.value?.click();
};

// 格式化文件大小
const formatFileSize = (bytes: number) => {
    if (bytes === 0) return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};

// 创建文件分片
const createFileChunks = (file: File) => {
    const chunks = [];
    let start = 0;
    let index = 0;

    while (start < file.size) {
        const end = Math.min(start + CHUNK_SIZE, file.size);
        const chunk = file.slice(start, end);
        chunks.push({
            index: index++,
            chunk: chunk,
            start: start,
            end: end,
        });
        start = end;
    }

    return chunks;
};

// 开始上传
const startUpload = async () => {
    if (!selectedFile.value) return;

    isUploading.value = true;
    isPaused.value = false;
    uploadResult.value = null;

    try {
        const fileHash = await calculateFileMD5(selectedFile.value);

        // 初始化上传
        const initResponse = await api.uploadInit({
            fileHash,
            fileName: selectedFile.value.name,
            totalChunks: totalChunks.value,
        });

        if (initResponse.data.uploaded) {
            // 秒传逻辑
            uploadProgress.value = 100;
            uploadResult.value = {
                success: true,
                message: '文件秒传成功！',
            };
            isUploading.value = false;
            return;
        }

        // 获取已上传的分片，返回的数据为： [0,1,2,3]
        const uploadedChunks = initResponse.data.uploadedChunks || [];
        uploadChunks.value = uploadedChunks;

        //  console.log('已上传的分片:', uploadedChunks);

        // 创建分片并过滤已上传的
        const allChunks = createFileChunks(selectedFile.value);

        // console.log(allChunks, 'allChunks');

        const chunksToUpload = allChunks.filter(chunk => !uploadedChunks.includes(chunk.index));

        // console.log(`需要上传 ${chunksToUpload.length} 个分片`);
        // 所有分片已上传，直接合并
        if (chunksToUpload.length === 0) {
            await mergeChunks(fileHash as string);
            return;
        }

        // 上传分片
        await uploadWithConcurrency([...chunksToUpload], fileHash as string, progress => {
            uploadProgress.value = progress;
        });

        // 合并分片
        if (!isPaused.value) {
            await mergeChunks(fileHash as string);
        }
    } catch (error: Error | any) {
        console.error('上传失败:', error);
        uploadResult.value = {
            success: false,
            message: `上传失败: ${error.message}`,
        };
    } finally {
        if (!isPaused.value) {
            isUploading.value = false;
        }
    }
};

// 并发控制上传
const uploadWithConcurrency = async (
    chunksToUpload: any,
    fileHash: string,
    onChunkProgress: (progress: number) => void
) => {
    let currentConcurrency = 0;
    let completedChunks = 0;

    const uploadNext = async () => {
        if (isPaused.value) return;

        if (chunksToUpload.length === 0) {
            if (currentConcurrency === 0) {
                onChunkProgress(100);
            }
            return;
        }

        if (currentConcurrency >= CONCURRENCY_LIMIT) return;

        currentConcurrency++;
        const chunk = chunksToUpload.shift();

        try {
            const formData = new FormData();
            formData.append('chunk', chunk.chunk);
            formData.append('chunkIndex', chunk.index);
            formData.append('fileHash', fileHash);
            formData.append('fileName', selectedFile.value.name);

            await api.uploadChunk(formData);

            completedChunks++;
            const overallProgress = ((completedChunks + uploadChunks.value.length) / totalChunks.value) * 100;
            onChunkProgress(overallProgress);
        } catch (error) {
            console.error(`分片 ${chunk.index} 上传失败:`, error);
            // 重试逻辑可以在这里实现
            chunksToUpload.push(chunk); // 重新加入队列等待重试
        } finally {
            currentConcurrency--;
            await uploadNext();
        }
    };

    // 启动并发上传
    const uploadPromises = [];
    for (let i = 0; i < CONCURRENCY_LIMIT; i++) {
        uploadPromises.push(uploadNext());
    }

    await Promise.all(uploadPromises);
};

// 合并分片
const mergeChunks = async (fileHash: string) => {
    try {
        await api.uploadMerge({
            fileHash,
            fileName: selectedFile.value.name,
            totalChunks: totalChunks.value,
        });

        uploadResult.value = {
            success: true,
            message: '文件上传并合并成功！',
        };
        // console.log('文件上传完成');
    } catch (error) {
        console.error('合并失败:', error);
        throw error;
    }
};

// 暂停上传
const pauseUpload = () => {
    isPaused.value = true;
    isUploading.value = false;
    uploadResult.value = {
        success: false,
        message: '上传已暂停',
    };
};
</script>

<style scoped>
.upload-container {
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
}

.upload-area {
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 40px;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.3s;
}

.upload-area:hover {
    border-color: #409eff;
}

.file-info {
    margin: 20px 0;
    padding: 15px;
    background: #f5f7fa;
    border-radius: 4px;
}

.upload-controls {
    margin: 20px 0;
}

.upload-btn,
.pause-btn {
    padding: 10px 20px;
    margin-right: 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.upload-btn {
    background: #409eff;
    color: white;
}

.upload-btn:disabled {
    background: #c0c4cc;
    cursor: not-allowed;
}

.pause-btn {
    background: #e6a23c;
    color: white;
}

.progress-section {
    margin: 20px 0;
}

.progress-bar {
    width: 100%;
    height: 20px;
    background: #f0f0f0;
    border-radius: 10px;
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    background: #67c23a;
    transition: width 0.3s;
}

.progress-text {
    display: block;
    text-align: center;
    margin-top: 5px;
}

.result-message {
    padding: 10px;
    border-radius: 4px;
    margin-top: 10px;
}

.result-message.success {
    background: #f0f9ff;
    color: #67c23a;
    border: 1px solid #c2e7b0;
}

.result-message.error {
    background: #fef0f0;
    color: #f56c6c;
    border: 1px solid #fbc4c4;
}
</style>
